package jxtend.processor.eclipse.utils;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import jxtend.processor.utils.ReflectUtils;

import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.codeassist.complete.CompletionOnQualifiedTypeReference;
import org.eclipse.jdt.internal.codeassist.select.SelectionOnParameterizedQualifiedTypeReference;
import org.eclipse.jdt.internal.codeassist.select.SelectionOnParameterizedSingleTypeReference;
import org.eclipse.jdt.internal.codeassist.select.SelectionOnQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ASTVisitor;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.ArrayQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ArrayTypeReference;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedSingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.BaseTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.PackageBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.RawTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;


@SuppressWarnings("restriction")
public final class ASTNodeUtils {

	public static TypeReference createTypeReferenceForBinding(
			ASTNodeKind astNodeKind, TypeBinding typeBinding, int sourceStart, int sourceEnd) {
		if (typeBinding instanceof ArrayBinding) {
			int arrayDimensions = ((ArrayBinding) typeBinding).dimensions;
			TypeBinding leafComponentType = ((ArrayBinding) typeBinding).leafComponentType;

			if (leafComponentType instanceof BaseTypeBinding) {
				char[] primitiveType = ((BaseTypeBinding) leafComponentType).simpleName;
				return new ArrayTypeReference(
						primitiveType, arrayDimensions, EclipseProcessorUtils.encodePos(sourceStart, sourceEnd));
			} else {
				assert EclipseProcessorUtils.isValidTypeReference(leafComponentType);

				char[][] compoundName = ((ReferenceBinding) leafComponentType).compoundName;
				return new ArrayQualifiedTypeReference(compoundName, arrayDimensions,
						encodePosArray(compoundName.length, sourceStart, sourceEnd));
			}
		} else if (typeBinding instanceof BaseTypeBinding) {
			BaseTypeBinding baseTypeBinding = (BaseTypeBinding) typeBinding;

			return ASTNodeUtils.createSingleTypeReference(astNodeKind, baseTypeBinding.simpleName,
					EclipseProcessorUtils.encodePos(sourceStart, sourceEnd));			
		} else {
			assert typeBinding instanceof ReferenceBinding;

			char[][] compoundName = ((ReferenceBinding) ((ReferenceBinding) typeBinding).erasure()).compoundName;
			
			if ((typeBinding instanceof ParameterizedTypeBinding) && !(typeBinding instanceof RawTypeBinding)) {
				ParameterizedTypeBinding parameterizedTypeBinding = (ParameterizedTypeBinding) typeBinding;

				return ASTNodeUtils.createParameterizedQualifiedTypeReference(
						astNodeKind, parameterizedTypeBinding, sourceStart, sourceEnd);
			} else {
				return ASTNodeUtils.createQualifiedTypeReference(astNodeKind, compoundName,
						encodePosArray(compoundName.length, sourceStart, sourceEnd));
			}
		}
	}
	
	public static char[][] compoundName(ReferenceBinding referenceBinding) {
		List<ReferenceBinding> nestedClassHierarchy = nestedClassHierarchy(referenceBinding);

		return compoundName(referenceBinding.getPackage(), nestedClassHierarchy);
	}

	private static char[][] compoundName(PackageBinding packageBinding,  List<ReferenceBinding> nestedClassHierarchy) {
		//TODO: test with default package
		char[][] packageName = packageBinding.compoundName;

		char[][] result = new char[packageName.length + nestedClassHierarchy.size()][];
		if (packageName.length != 0) {
			System.arraycopy(packageName, 0, result, 0, packageName.length);
		}
		for (int i = 0; i < nestedClassHierarchy.size(); i++) {
			result[packageName.length + i] = nestedClassHierarchy.get(i).sourceName;
		}

		return result;
	}

	private static List<ReferenceBinding> nestedClassHierarchy(ReferenceBinding possibleNestedClass) {
		List<ReferenceBinding> result = new ArrayList<ReferenceBinding>();
		result.add(possibleNestedClass);

		ReferenceBinding currentType = possibleNestedClass.enclosingType();

		while (currentType != null) {
			result.add(0, currentType);

			currentType = currentType.enclosingType();
		}

		return result;
	}

	private static TypeReference[][] createQualifiedTypeArguments(ASTNodeKind kind,
			ParameterizedTypeBinding parameterizedTypeBinding, int sourceStart, int sourceEnd) {
		PackageBinding packageBinding = parameterizedTypeBinding.getPackage();
		
		List<ReferenceBinding> nestedClassHierarchy = nestedClassHierarchy(parameterizedTypeBinding);

		//TODO: test with default package
		char[][] packageName = packageBinding.compoundName;

		int indexOfMostOuterClass = packageName.length;
		int typeArgumentsArrayLength = indexOfMostOuterClass + nestedClassHierarchy.size();

		TypeReference[][] result = new TypeReference[typeArgumentsArrayLength][];

		int typeArgumentIndex = indexOfMostOuterClass;
		for (Iterator<ReferenceBinding> iterator = nestedClassHierarchy.iterator(); iterator.hasNext();
				typeArgumentIndex++) {
			ReferenceBinding type = iterator.next();
			if (!(type instanceof ParameterizedTypeBinding) || (type instanceof RawTypeBinding)) { continue; }

			ParameterizedTypeBinding parameterizedType = (ParameterizedTypeBinding) type;
			TypeBinding[] arguments = parameterizedType.arguments;

			result[typeArgumentIndex] = new TypeReference[arguments.length];
			
			for (int i = 0; i < arguments.length; i++) {
				result[typeArgumentIndex][i] =
					createTypeReferenceForBinding(kind, arguments[i], sourceStart, sourceEnd); 		
			}
		}

		return result;
	}

	public static long[] encodePosArray(int length, int sourceStart, int sourceEnd) {
		long[] result = new long[length];
		for (int i = 0; i < length; i++) {
			result[i] = EclipseProcessorUtils.encodePos(sourceStart, sourceEnd);
		}

		return result;
	}
	
	public static long[] encodePosArray(int length, long pos) {
		long[] result = new long[length];
		for (int i = 0; i < length; i++) {
			result[i] = pos;
		}

		return result;
	}

	public static SingleTypeReference createSingleTypeReference(ASTNodeKind kind, char[] source, long pos) {
		Class<SingleTypeReference> clazz = kind.getClassForKind(SingleTypeReference.class);
		Constructor<SingleTypeReference> constructor =
			ReflectUtils.getPublicConstructor(clazz, char[].class, long.class);

		return ReflectUtils.newInstance(constructor, source, pos);
	}

	public static ParameterizedQualifiedTypeReference createParameterizedQualifiedTypeReference(
			ASTNodeKind kind, ParameterizedTypeBinding parameterizedTypeBinding, //TypeReference[][] typeArguments,
			int sourceStart, int sourceEnd) {
//			int dim, long[] positions) {

		char[][] compoundName = compoundName(parameterizedTypeBinding);
		TypeReference[][] typeArguments =
				createQualifiedTypeArguments(kind, parameterizedTypeBinding, sourceStart, sourceEnd);

		return createParameterizedQualifiedTypeReference(kind, compoundName, typeArguments, sourceEnd, sourceEnd);
	}

	public static ParameterizedQualifiedTypeReference createParameterizedQualifiedTypeReference(
			ASTNodeKind kind, char[][] compoundName, TypeReference[][] typeArguments, int sourceStart, int sourceEnd) {
		long[] positions = encodePosArray(typeArguments.length, sourceStart, sourceEnd);

		Class<ParameterizedQualifiedTypeReference> clazz =
				kind.getClassForKind(ParameterizedQualifiedTypeReference.class);

		if (clazz.equals(SelectionOnParameterizedQualifiedTypeReference.class)) {
			return ReflectUtils.newInstance(
					ReflectUtils.getPublicConstructor(clazz,
							char[][].class, char[].class, TypeReference[][].class, TypeReference[].class, long[].class),
					CharOperation.subarray(compoundName, 0, compoundName.length - 1),
					compoundName[compoundName.length - 1], typeArguments, null, positions);
		} else {
			return ReflectUtils.newInstance(
					ReflectUtils.getPublicConstructor(
							clazz, char[][].class, TypeReference[][].class, int.class, long[].class),
					compoundName, typeArguments, 0, positions);
		}
	}

	public static QualifiedTypeReference createQualifiedTypeReference(
			ASTNodeKind kind, char[][] sources, long[] poss) {
		Class<QualifiedTypeReference> clazz = kind.getClassForKind(QualifiedTypeReference.class);
		
		if (clazz.equals(SelectionOnQualifiedTypeReference.class) ||
				clazz.equals(CompletionOnQualifiedTypeReference.class)) {
			return ReflectUtils.newInstance(
					ReflectUtils.getPublicConstructor(clazz, char[][].class, char[].class, long[].class),
					CharOperation.subarray(sources, 0, sources.length - 1), sources[sources.length - 1], poss);
		} else {
			return ReflectUtils.newInstance(
					ReflectUtils.getPublicConstructor(clazz, char[][].class, long[].class), sources, poss);
		}
	}

	public static ParameterizedSingleTypeReference createParameterizedSingleTypeReference(
			ASTNodeKind kind, char[] name, TypeReference[] typeArguments, int dim, long pos) {
		Class<ParameterizedSingleTypeReference> clazz = kind.getClassForKind(ParameterizedSingleTypeReference.class);

		if (clazz.equals(SelectionOnParameterizedSingleTypeReference.class)) {
			return ReflectUtils.newInstance(
					ReflectUtils.getPublicConstructor(clazz, char[].class, TypeReference[].class, long.class),
					name, typeArguments, pos);
		} else {
			return ReflectUtils.newInstance(
					ReflectUtils.getPublicConstructor(
							clazz, char[].class, TypeReference[].class, int.class, long.class),
					name, typeArguments, dim, pos);
		}		
	}

	public static Argument createArgument(ASTNodeKind kind, char[] name, long posNom, TypeReference tr, int modifiers) {
		Class<Argument> clazz = (kind == ASTNodeKind.NORMAL)
				? kind.getClassForKind(Argument.class) : kind.getClassForKind(Argument.class, "Name");

		return ReflectUtils.newInstance(
				ReflectUtils.getPublicConstructor(clazz, char[].class, long.class, TypeReference.class, int.class),
				name, posNom, tr, modifiers);
	}
	
	public static <T extends ASTNode> void traverseNullableNode(T node, ASTVisitor visitor, BlockScope scope) {
		if (node == null) {
			return;
		}

		node.traverse(visitor, scope);
	}

	public static <T extends Expression> void traverseNullableNode(T node, ASTVisitor visitor, ClassScope scope) {
		if (node == null) {
			return;
		}

		node.traverse(visitor, scope);
	}

	public static <T extends ASTNode> void traverseArray(T[] array, ASTVisitor visitor, BlockScope scope) {
		if (array == null) {
			return;
		}

		for (int i = 0; i < array.length; i++) {
			array[i].traverse(visitor, scope);
		}
	}

	public static <T extends Expression> void traverseArray(T[] array, ASTVisitor visitor, ClassScope scope) {
		if (array == null) {
			return;
		}

		for (int i = 0; i < array.length; i++) {
			array[i].traverse(visitor, scope);
		}
	}

	public static <A extends ASTNode> Class<A> getClassForNormalKindASTNode(ASTNode astNode) {
		if (astNode.getClass().getSimpleName().startsWith("SelectionOn")
				|| astNode.getClass().getSimpleName().startsWith("CompletionOn")) {
			@SuppressWarnings("unchecked") Class<A> result = (Class<A>) astNode.getClass().getSuperclass();
			return result;
		}

		@SuppressWarnings("unchecked") Class<A> result = (Class<A>) astNode.getClass();
		return result;
	}

	private ASTNodeUtils() {
		// Utility class
	}

}

	